Skip to content
This repository has been archived by the owner on Mar 16, 2024. It is now read-only.

Resolved offerings #2369

Merged
merged 8 commits into from
Dec 13, 2023
Merged

Resolved offerings #2369

merged 8 commits into from
Dec 13, 2023

Conversation

g-linville
Copy link
Contributor

@g-linville g-linville commented Dec 5, 2023

Overview

This change refactors AppInstance.Status.Defaults to AppInstance.Status.ResolvedOfferings. This field used to store the default values for container memory requirements, the region, and volumeclass information for the AppInstance. With these changes, the ResolvedOfferings stores the actual value of the app's region, computeclass information for each container in the app (rather than just memory information), and volumeclass information for each volume. Now it is the source of absolute truth, and there is no need to compare AppInstance.Spec (overrides from the user), AppInstance.Status.AppSpec (defaults from the acorn image), and AppInstance.Status.Defaults (defaults from volume and compute classes) like we used to.

This is a breaking API change. I have updated all of the unit tests in here to reflect that change. (Sorry, there are a lot of them.) I believe this will not cause deployed apps to restart, but they will not get the status updates until they are processed again by the controller (which requires their generation to increase). If we want them to all restart and get the status updates as soon as this runtime change rolls out, I can do that pretty easily as well.

Checklist

  • The title of this PR would make a good line in Acorn's Release Note's Changelog
  • The title of this PR ends with a link to the main issue being address in parentheses, like: This is a title (#1216). Here's an example
  • All relevant issues are referenced in the PR description. NOTE: don't use GitHub keywords that auto-close issues
  • Commits follow contributing guidance
  • Automated tests added to cover the changes. If tests couldn't be added, an explanation is provided in the Verification and Testing section
  • Changes to user-facing functionality, API, CLI, and upgrade impacts are clearly called out in PR description
  • PR has at least two approvals before merging (or a reasonable exception, like it's just a docs change)

type ResolvedOfferings struct {
VolumeSize *resource.Quantity `json:"volumeSize,omitempty"`
Volumes map[string]VolumeResolvedOffering `json:"volumes,omitempty"`
Containers map[string]ContainerResolvedOffering `json:"containers,omitempty"`
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Instead of storing a map of container name to memory request, we're storing a map of container name to a struct containing the following info:

  • memory request
  • computeclass name
  • cpuscaler value

Comment on lines 615 to 617
if offerings := resolvedOfferingsAnnotation(appInstance, container); offerings != "" {
annotations[labels.AcornContainerResolvedOfferings] = offerings
}
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I need to put this annotation onto the ContainerReplica so that I can determine the name of the container's ComputeClass in something else I am working on, where the only info that I have is the ContainerReplica and not the corresponding AppInstance.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

renamed this file to computeclass.go and heavily modified it, so git just thinks I deleted it

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this test no longer tests anything useful so I just deleted it

Comment on lines -16 to -19
func TestVolumeClassDefaultsSet(t *testing.T) {
tester.DefaultTest(t, scheme.Scheme, "testdata/volumeclass/volume-class-defaults-set", Calculate)
}

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

test was no longer useful; deleted

@g-linville g-linville force-pushed the resolved-offerings branch 2 times, most recently from d06e018 to dcafa98 Compare December 6, 2023 22:15
@g-linville g-linville changed the title Draft: Resolved offerings Resolved offerings Dec 6, 2023
@g-linville g-linville marked this pull request as ready for review December 6, 2023 22:44
} else {
in.Status.Defaults.Region = ""
}
in.Status.ResolvedOfferings.Region = region
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The intention here was that we only set the region here if it is blank. Some of our code relies on this (e.g. I can call SetDefaultRegion as many times as I want and it will do the right thing).

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@thedadams let me know if this function looks good to you now. I am still a little bit confused as to how it is supposed to work to be honest.

Comment on lines 602 to 603
data, _ := convert.EncodeToMap(resolved)
j, _ := json.Marshal(data)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not a big fan of ignoring errors here.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

👍 yeah, should always handle error in case of bad inputs

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed

pkg/controller/scheduling/scheduling.go Outdated Show resolved Hide resolved
@@ -258,7 +258,7 @@ func (s *Validator) ValidateUpdate(ctx context.Context, obj, old runtime.Object)
}
}

if newParams.Spec.Region != oldParams.Spec.Region && newParams.Spec.Region != oldParams.Status.Defaults.Region {
if newParams.Spec.Region != oldParams.Spec.Region && newParams.Spec.Region != oldParams.Status.ResolvedOfferings.Region {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit

Suggested change
if newParams.Spec.Region != oldParams.Spec.Region && newParams.Spec.Region != oldParams.Status.ResolvedOfferings.Region {
if newParams.Spec.Region != oldParams.Status.ResolvedOfferings.Region {

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Didn't go with the suggestion because it caused update validation issues, but I did modify this line. Let me know what you think.

Comment on lines 155 to 158
// Order of precedence:
// 1. VolumeResolvedOffering - the previously resolved values for Class, Size, and AccessModes
// 2. VolumeBinding - the values set by the user in the app spec
// 3. VolumeRequest - the defaults as defined in the acorn image
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Volume bindings are the ones set by the user at runtime (with the -v flag). I think these are the top precedence.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You're correct. I completely reworked this function.

pkg/apis/api.acorn.io/v1/types.go Show resolved Hide resolved
Comment on lines 602 to 603
data, _ := convert.EncodeToMap(resolved)
j, _ := json.Marshal(data)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

👍 yeah, should always handle error in case of bad inputs

Copy link
Contributor

@tylerslaton tylerslaton left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Took a look over and the implementation looks correct. I do want to give this a test in my side but approving so you're not blocked on your other work.

}
}
pvc.Spec.Resources.Requests[corev1.ResourceStorage] = defaultSize
pvc.Spec.Resources.Requests[corev1.ResourceStorage] = *v1.DefaultSize
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why is the above validation no longer required?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not sure why I removed that, lol. I'll add it back.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oh wait, nvm. The reason I removed that is because the refactored ResolveVolumeRequest function (called earlier here) will handle setting the default size and checking to see if there is already a size that was previously resolved. So by the time we get down here, if there is still no determined size, the only thing left to use is v1.DefaultSize.

Signed-off-by: Grant Linville <[email protected]>
@thedadams thedadams self-requested a review December 11, 2023 12:44
Comment on lines 67 to 70
if in.Spec.Region != "" && in.Status.ResolvedOfferings.Region == "" {
in.Status.ResolvedOfferings.Region = in.Spec.Region
} else {
in.Status.ResolvedOfferings.Region = region
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This logic isn't quite right. For example, if spec.region and status.resolvedOfferings.region are both not empty, then status.resolvedOfferings.region gets set to the passed in region.

I think this is correct:

Suggested change
if in.Spec.Region != "" && in.Status.ResolvedOfferings.Region == "" {
in.Status.ResolvedOfferings.Region = in.Spec.Region
} else {
in.Status.ResolvedOfferings.Region = region
if in.Spec.Region != "" {
in.Status.ResolvedOfferings.Region = in.Spec.Region
} else if in.Status.ResolvedOfferings.Region == "" {
in.Status.ResolvedOfferings.Region = region

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I reworked it a little bit. Let me know if you think it looks good. I'm still kinda confused about this.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Honestly, I think what I suggested is simpler. However, what you have does the job.


bound := volumeBindings[name].Volume != ""

if _, alreadySet := app.Status.ResolvedOfferings.Volumes[name]; !alreadySet && !bound {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This makes it seem like it is not possible to change a volume's settings. For example, if a volume as been resolved and is alreadySet, then would it be possible to change its size or access modes?

The difference of what this did before was that it was only calculating the defaults, and we decided that the defaults for a volume shouldn't change because it causes too many issues. However, now this is the completely "resolved" volume information, which should be allowed to change if the user tries to change it via an Acornfile update (I think your logic allows the user to change volume information via the command line).

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oh, good point. I will work on fixing this.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So, from what I understand, you cannot change the access modes of an existing PVC in Kubernetes, but you can increase its size. I heavily modified this function so that the size can be changed even if the volume offering has been previously resolved, but the volume class and access modes cannot be changed after the volume is created. Let me know if it looks good to you. From my testing, everything seems to me to be working properly.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It doesn't seem to set a different size if a volume is alreadySet and not bound.

@@ -151,24 +151,30 @@ func getVolumeClassNames(volumeClasses map[string]adminv1.ProjectVolumeClassInst
return typed.SortedKeys(storageClassName)
}

func CopyVolumeDefaults(volumeRequest v1.VolumeRequest, volumeBinding v1.VolumeBinding, volumeDefaults v1.VolumeDefault) v1.VolumeRequest {
func ResolveVolumeRequest(volumeRequest v1.VolumeRequest, volumeBinding v1.VolumeBinding, resolvedVolume v1.VolumeResolvedOffering) v1.VolumeRequest {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The previous iteration of this function had one responsibility: to put the request, binding and default together to get a merged volume request for the volume to process.

It seems that now this doesn't completely "resolve" the volume. It resolves the request without the defaults and the default information is added later, if needed. In other words, it kind of only does have the job now.

My opinion, which can be completely disregarded, is that passing the default information here and using it to complete "resolve" the volume would be better. Some care would need to be taken so that this function can be uses in the various places it is being used, but I think it would make it more complete.

Copy link
Contributor Author

@g-linville g-linville Dec 12, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I reworked the function to fully resolve the volume based on the previously resolved values, all compute class information, bindings specified by the user, etc. Thankfully it didn't require too many modifications in the other places where this function is called. I agree that it is better this way.

Signed-off-by: Grant Linville <[email protected]>
Signed-off-by: Grant Linville <[email protected]>
Signed-off-by: Grant Linville <[email protected]>
func podAnnotations(appInstance *v1.AppInstance, container v1.Container) map[string]string {
annotations := map[string]string{
labels.AcornContainerSpec: containerAnnotation(container),
}
addPrometheusAnnotations(annotations, container)

offerings, err := resolvedOfferingsAnnotation(appInstance, container)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks like this error is still not handled...

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Below this line there is a check for if err == nil, and then we attach the annotation. I don't actually want to return this error if it happens, since we can safely just ignore it and keep going.

@@ -52,23 +52,23 @@ type AppInstance struct {
}

func (in *AppInstance) HasRegion(region string) bool {
return in.Status.Defaults.Region == region || in.Spec.Region == region
return in.Status.ResolvedOfferings.Region == region || in.Spec.Region == region
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
return in.Status.ResolvedOfferings.Region == region || in.Spec.Region == region
return in.Status.ResolvedOfferings.Region == region

Comment on lines 59 to 62
if in.Status.ResolvedOfferings.Region != "" {
return in.Status.ResolvedOfferings.Region
}
return in.Status.Defaults.Region
return in.Spec.Region
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we can just return in.Status.ResolvedOfferings.Region here.


bound := volumeBindings[name].Volume != ""

if _, alreadySet := app.Status.ResolvedOfferings.Volumes[name]; !alreadySet && !bound {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It doesn't seem to set a different size if a volume is alreadySet and not bound.

@g-linville g-linville merged commit dbd950d into acorn-io:main Dec 13, 2023
5 checks passed
g-linville added a commit to g-linville/runtime that referenced this pull request Dec 13, 2023
g-linville added a commit to g-linville/runtime that referenced this pull request Dec 13, 2023
This reverts commit dbd950d.

Signed-off-by: Grant Linville <[email protected]>
g-linville added a commit that referenced this pull request Dec 13, 2023
This reverts commit dbd950d.

Signed-off-by: Grant Linville <[email protected]>
g-linville added a commit to g-linville/runtime that referenced this pull request Dec 14, 2023
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants